package ch.hslu.gl20vscpudemo;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import ch.hslu.common.ESShader;
import ch.hslu.common.RawResourceReader;

public class GLES20Renderer implements GLSurfaceView.Renderer {

	// interfaces ////////////////////////////////////////////////////////////////
	interface Callback {
		void onBitmap(Bitmap bmp);
	}

	// members ///////////////////////////////////////////////////////////////////

	private final Context mContext;
	private EGLConfig mEGLConfig;

	// Shader programs
	private final String mSrcVertexShader;
	private final String mSrcFragmentShader;

	private final float[] mDataVertices;
	private final FloatBuffer mVertices;
	private final short[] mDataIndices;
	private final ShortBuffer mIndices;

	private int nTexW; // texture width
	private int nTexH; // texture height

	// filter default values: x, y, w 
	private float[] mDataTexelPosWeight =
		{
		    0.0f, 0.0f, 1.0f
		};

	// vertex/fragment object handlers
	private int mProgObj;

	// vertex/fragment attribute, uniform, texture locations
	private int mLocPosition;
	private int mLocTexCoord;
	private int mLocBaseMap;
	private int mLocTexture; // texture location
	private int mLocTexelPosWeight; // texel position and weight location

	private final Callback mCallback;
	private ByteBuffer bbImage;
	private int nCountOnDraw;

	// constructor ///////////////////////////////////////////////////////////////
	/**
	 * initialize openGL renderer
	 * @param context activity
	 * @param cb callback method
	 * @param w image width
	 * @param h image height
	 * @param a2k kernel
	 */
	public GLES20Renderer(Context context, GLES20Renderer.Callback cb, int w, int h, float[][] a2k) {
		mContext = context;
		mCallback = cb;

		// create an empty float buffer
		mDataVertices = getVerticesData(1.0f, 1.0f, 0.0f);
		mVertices = ByteBuffer.allocateDirect(mDataVertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
		mVertices.put(mDataVertices).position(0);

		// create an empty short buffer
		mDataIndices = getIndicesData(0, 1, 2, 0, 2, 3); // index
		mIndices = ByteBuffer.allocateDirect(mDataIndices.length * 3).order(ByteOrder.nativeOrder()).asShortBuffer();
		mIndices.put(mDataIndices).position(0);

		// store width and height from texture
		nTexW = w;
		nTexH = h;

		// initialize filter kernel
		loadKernel2d(a2k);

		// load sourcecode from raw
		mSrcVertexShader = RawResourceReader.readTextFileFromRawResource(mContext, R.raw.vertex_shader);
		mSrcFragmentShader = "#define IMAGE_NUM_OF_FILT_ELEMENTS " + Integer.toString(mDataTexelPosWeight.length) + " \n"
		    + "#define IMAGE_WIDTH " + Integer.toString(nTexW) + " \n"
				+ "#define IMAGE_HEIGHT " + Integer.toString(nTexH) +" \n"
		    + RawResourceReader.readTextFileFromRawResource(mContext, R.raw.fragment_shader);

		// default values
		bbImage = null; // no image availible
		nCountOnDraw = 0; // count drawings
	}

	// public methods ////////////////////////////////////////////////////////////

	// Draw a triangle using the shader pair created in onSurfaceCreated()
	public void onDrawFrame(GL10 glUnused) {
		// Set the viewport
		GLES20.glViewport(0, 0, nTexW, nTexH);

		// Clear the color buffer
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

		// Use the program object
		GLES20.glUseProgram(mProgObj);

		// Load the vertex position
		mVertices.position(0);
		GLES20.glVertexAttribPointer(mLocPosition, 3, GLES20.GL_FLOAT, false, 5 * 4, mVertices);
		// Load the texture coordinate
		mVertices.position(3);
		GLES20.glVertexAttribPointer(mLocTexCoord, 2, GLES20.GL_FLOAT, false, 5 * 4, mVertices);

		GLES20.glEnableVertexAttribArray(mLocPosition);
		GLES20.glEnableVertexAttribArray(mLocTexCoord);

		// Bind the base map
		GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mLocTexture);

		// Set the base map sampler to texture unit to 0
		GLES20.glUniform1i(mLocBaseMap, 0);

		// Set the weight values
		// remember: pos x, pos y, weight
		GLES20.glUniform1fv(mLocTexelPosWeight, mDataTexelPosWeight.length, mDataTexelPosWeight ,0);

		GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, mIndices);

		nCountOnDraw++;
		if (1 < nCountOnDraw) {
			// second onDrawFrame was called
			mCallback.onBitmap(getBitmap());
		}
	}

	// Handle surface changes
	/** GLSurfaceView.Renderer */
	public void onSurfaceChanged(GL10 glUnused, int w, int h) {
		nTexW = w;
		nTexH = h;
	}

	// Initialize the shader and program object
	public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
		mEGLConfig = config;
		// Load the shaders and get a linked program object
		mProgObj = ESShader.loadProgram(mSrcVertexShader, mSrcFragmentShader);

		// Get the attribute locations
		mLocPosition = GLES20.glGetAttribLocation(mProgObj, "a_position");
		mLocTexCoord = GLES20.glGetAttribLocation(mProgObj, "a_texCoord");

		// Get the sampler location
		mLocBaseMap = GLES20.glGetUniformLocation(mProgObj, "s_baseMap");
		// Get the weight and selector location
		mLocTexelPosWeight = GLES20.glGetUniformLocation(mProgObj, "fTexSelWeight");

		mLocTexture = loadTexture();
		GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	}

	/**
	 * push image to gpu
	 * 
	 * @param bbI Image
	 */
	public void pushImage(ByteBuffer bbI) {
		bbImage = bbI;
		onSurfaceCreated(null, mEGLConfig);
	}

	public Bitmap getBitmap() {
		// solution idea from:
		// http://permalink.gmane.org/gmane.comp.handhelds.android.devel/95258
		int w = nTexW;
		int h = nTexH;
		int x = 0;
		int y = 0;
		int b[] = new int[w * (y + h)];
		int bt[] = new int[w * h];
		IntBuffer ib = IntBuffer.wrap(b);
		ib.position(0);
		GLES20.glReadPixels(x, 0, w, y + h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, ib);
		for (int i = 0, k = 0; i < h; i++, k++) {
			// remember, that OpenGL bitmap is incompatible with Android bitmap
			// and so, some correction need.
			for (int j = 0; j < w; j++) {
				int pix = b[i * w + j];
				int pb = (pix >> 16) & 0xff;
				int pr = (pix << 16) & 0x00ff0000;
				int pix1 = (pix & 0xff00ff00) | pr | pb;
				bt[(h - k - 1) * w + j] = pix1;
			}
		}
		Bitmap sb = Bitmap.createScaledBitmap(Bitmap.createBitmap(bt, w, h, Bitmap.Config.RGB_565), nTexW, nTexH, false);
		return sb;
	}

	// private methods ///////////////////////////////////////////////////////////
	private final float[] getVerticesData(final float x, final float y, final float z) {
		final float[] fData =
			{
			    -x, y, z, // Position 0
			    0.0f, 0.0f, // TexCoord 0
			    -x, -y, z, // Position 1
			    0.0f, 1.0f, // TexCoord 1
			    x, -y, z, // Position 2
			    1.0f, 1.0f, // TexCoord 2
			    x, y, z, // Position 3
			    1.0f, 0.0f
			// TexCoord 3
			};
		return fData;
	}

	private final short[] getIndicesData(final int p0, final int p1, final int p2, final int p3, final int p4,
	    final int p5) {
		final short[] sData =
			{
			    (short) p0, (short) p1, (short) p2, (short) p3, (short) p4, (short) p5
			};
		return sData;
	}

	private void loadKernel2d(float[][] a2Kernel) {
		int nYlength = a2Kernel.length;
		int nXlength = a2Kernel[0].length;
		// create moves and weights
		mDataTexelPosWeight = new float[nXlength * nYlength * 3];
		for (int y = 0; y < nYlength; y++) {
			for (int x = 0; x < nXlength; x++) {
				mDataTexelPosWeight [(((y * nXlength) + x) * 3) + 0] = (float) ((int) (x - (nXlength / 2)));
				mDataTexelPosWeight [(((y * nXlength) + x) * 3) + 1] = (float) ((int) (y - (nYlength / 2)));
				mDataTexelPosWeight [(((y * nXlength) + x) * 3) + 2] = a2Kernel[y][x];
			}
		}
	}

	private int loadTexture() {
		final int[] idTex = new int[1];
		// generate the textures
		GLES20.glGenTextures(idTex.length, idTex, 0); // num of textures, location,
		                                              // offset
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, idTex[0]);

		GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, nTexW, nTexH, 0, GLES20.GL_RGB,
		    GLES20.GL_UNSIGNED_BYTE, bbImage);

		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
		GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
		return idTex[0];
	}
}
